package com.crawler.util.contex.impl;

import com.crawler.util.StrKit;
import org.jsoup.Jsoup;
import org.jsoup.nodes.*;
import org.jsoup.parser.Tag;

import java.util.Objects;

/**
 * 基于html标签清理器，已知tag及其属性全部保留，支持html5的tag
 *
 * @author guor
 * @date 2016年2月14日 下午2:51:31
 */
public final class TagBaseCleaner {

    public static Document clean(String html, String url) {
        return clean(Jsoup.parse(html, url));
    }

    public static Document clean(Document dirtyDocument) {
        Objects.requireNonNull(dirtyDocument);

        Document clean = Document.createShell(dirtyDocument.baseUri());
        if (dirtyDocument.body() != null) {
            copySafeNodes(dirtyDocument.body(), clean.body());
        }
        return clean;
    }

    private static final class CleaningVisitor {
        private Element destination;

        private CleaningVisitor(Element destination) {
            this.destination = destination;
        }

        /**
         * 如果一个不是有效tag，则返回false，不遍历子节点
         */
        public boolean head(Node source, int depth) {
            if (source instanceof Element) {
                Element sourceEl = (Element) source;
                boolean isHidden = isHidden(sourceEl);
                if (isHidden || !Tag.isKnownTag(sourceEl.tagName())) {
                    return false;
                }
                Element destChild = createSafeElement(sourceEl);
                destination.appendChild(destChild);
                destination = destChild;
            } else if (source instanceof TextNode) {
                TextNode sourceText = (TextNode) source;
                TextNode destText = new TextNode(sourceText.getWholeText(), source.baseUri());
                destination.appendChild(destText);
            } else if (source instanceof DataNode && Tag.isKnownTag(source.parent().nodeName())) {
                DataNode sourceData = (DataNode) source;
                DataNode destData = new DataNode(sourceData.getWholeData(), source.baseUri());
                destination.appendChild(destData);
            } else if (source instanceof Comment) {
                // 注释节点，丢掉
                return false;
            } else {
                return false;
            }
            return true;
        }

        public void tail(Node source, int depth) {
            if (source instanceof Element && Tag.isKnownTag(source.nodeName()) && !isHidden((Element) source)) {
                destination = destination.parent();
            }
        }
    }

    /**
     * 粗略判断是否隐藏元素 1. class="hidden" 2. style="display:none;"
     */
    private static boolean isHidden(Element el) {
        boolean isHidden = el.hasClass("hidden");
        if (isHidden) {
            return true;
        }
        String style = el.attr("style");
        return !StrKit.isEmpty(style) && style.matches("[\\s\\S]*display:\\s?none[\\s\\S]*");
    }

    private static final class NodeTraversor {
        private final CleaningVisitor visitor;

        public NodeTraversor(CleaningVisitor visitor) {
            this.visitor = visitor;
        }

        /**
         * 短路深度遍历：
         *
         * <pre>
         *    A
         *   / \
         *  B   C
         * </pre>
         * <p>
         * 访问A，成功则继续访问B，失败则访问A的兄弟节点，Jsoup默认实现中，如果访问A失败会去访问其子节点
         */
        public void traverse(Node root) {
            Node node = root.childNode(0);
            int depth = 1;

            while (node != null) {
                boolean t = visitor.head(node, depth);
                if (node.childNodeSize() > 0 && t) {
                    node = node.childNode(0);
                    depth++;
                } else {
                    while (node.nextSibling() == null && depth > 0) {
                        visitor.tail(node, depth);
                        node = node.parentNode();
                        depth--;
                    }
                    visitor.tail(node, depth);
                    if (node == root) {
                        break;
                    }
                    node = node.nextSibling();
                }
            }
        }
    }

    private static void copySafeNodes(Element source, Element dest) {
        CleaningVisitor cleaningVisitor = new CleaningVisitor(dest);
        NodeTraversor nodeTraversor = new NodeTraversor(cleaningVisitor);
        nodeTraversor.traverse(source);
    }

    private static Element createSafeElement(Element sourceEl) {
        String sourceTag = sourceEl.tagName();
        Attributes destAttrs = new Attributes();
        Element dest = new Element(Tag.valueOf(sourceTag), sourceEl.baseUri(), destAttrs);

        Attributes sourceAttrs = sourceEl.attributes();
        for (Attribute sourceAttr : sourceAttrs) {
            destAttrs.put(sourceAttr);
        }
        return dest;
    }
}
